import minimist from "minimist";
import chalk from "chalk";
import prompts from "prompts";
import path from "node:path";
import { fileURLToPath } from "node:url";
import fs from "node:fs";

// 支持 help、template 两个选项，并且有别名 h 和 t
const argv = minimist<{
  template?: string;
  help?: boolean;
}>(process.argv.slice(2), {
  alias: { h: "help", t: "template" },
  string: ["_"],
});

const helpMessage = `\
Usage: create-vite [OPTION]... [DIRECTORY]

Create a new Vite project in JavaScript or TypeScript.
With no arguments, start the CLI in interactive mode.

Options:
  -t, --template NAME        use a specific template

Available templates:
${chalk.yellow("vanilla-ts     vanilla")}
${chalk.green("vue-ts         vue")}
${chalk.cyan("react-ts       react")}
${chalk.cyan("react-swc-ts   react-swc")}
${chalk.magenta("preact-ts      preact")}
${chalk.redBright("lit-ts         lit")}
${chalk.red("svelte-ts      svelte")}
${chalk.blue("solid-ts       solid")}
${chalk.blueBright("qwik-ts        qwik")}`;

function formatTargetDir(targetDir: string | undefined) {
  return targetDir?.trim().replace(/\/+$/g, "");
}

type Framework = {
  name: string;
  display: string;
  color: Function;
  variants: FrameworkVariant[];
};

type FrameworkVariant = {
  name: string;
  display: string;
  color: Function;
  customCommand?: string;
};
const FRAMEWORKS: Framework[] = [
  {
    name: "vue",
    display: "Vue",
    color: chalk.green,
    variants: [
      {
        name: "vue-ts",
        display: "TypeScript",
        color: chalk.blue,
      },
      {
        name: "vue",
        display: "JavaScript",
        color: chalk.yellow,
      },
    ],
  },
  {
    name: "react",
    display: "React",
    color: chalk.cyan,
    variants: [
      {
        name: "react-ts",
        display: "TypeScript",
        color: chalk.blue,
      },
      {
        name: "react-swc-ts",
        display: "TypeScript + SWC",
        color: chalk.blue,
      },
      {
        name: "react",
        display: "JavaScript",
        color: chalk.yellow,
      },
      {
        name: "react-swc",
        display: "JavaScript + SWC",
        color: chalk.yellow,
      },
    ],
  },
];

const TEMPLATES = FRAMEWORKS.map((f) => {
  return f.variants?.map((v) => v.name);
}).reduce((a, b) => {
  return a.concat(b);
}, []);

// console.log(FRAMEWORKS, TEMPLATES);

const defaultTargetDir = "vite-project";

async function init() {
  const argTargetDir = formatTargetDir(argv._[0]);
  const argTemplate = argv.template || argv.t;

  const help = argv.help;
  if (help) {
    console.log(helpMessage);
    return;
  }

  let targetDir = argTargetDir || defaultTargetDir;

  let result: prompts.Answers<"projectName" | "framework" | "variant">;

  try {
    result = await prompts(
      [
        {
          // type 是指定类型，比如 text、select，当指定为 null 的时候就会忽略这个问题。如果参数传入了 projectName，那自然就不用提问了，这时候就设置 type 为 null。
          type: argTargetDir ? null : "text",
          name: "projectName",
          //   chalk.reset 是重置颜色的意思，不受之前设置的颜色的影响
          message: chalk.reset("Project name:"),
          initial: defaultTargetDir,
          onState: (state) => {
            targetDir = formatTargetDir(state.value) || defaultTargetDir;
          },
        },
        {
          type:
            argTemplate && TEMPLATES.includes(argTemplate) ? null : "select",
          name: "framework",
          message: chalk.reset("Select a framework:"),
          initial: 0,
          choices: FRAMEWORKS.map((framework) => {
            const frameworkColor = framework.color;
            return {
              title: frameworkColor(framework.display || framework.name),
              value: framework,
            };
          }),
        },
        {
          // 第二个 type 是个函数，当 type 为函数的时候，参数为上个问题的答案，所以这里可以根据 framework.variants 是否存在来决定是否显示。
          type: (framework: Framework) =>
            framework && framework.variants ? "select" : null,
          name: "variant",
          message: chalk.reset("Select a variant:"),
          choices: (framework: Framework) =>
            framework.variants.map((variant) => {
              const variantColor = variant.color;
              return {
                title: variantColor(variant.display || variant.name),
                value: variant.name,
              };
            }),
        },
      ],
      {
        onCancel: () => {
          throw new Error(chalk.red("✖") + " Operation cancelled");
        },
      }
    );

    const { framework, variant } = result;
    // process.cwd() 是执行命令的目录, 然后拼上 targetDir 就是目标目录。
    const root = path.join(process.cwd(), targetDir);

    let template: string = variant || argTemplate;

    console.log(`\nScaffolding project in ${root}...`);

    // import.meta.url 就是当前文件的路径, 不过是 file:/// 开头的，可以用 fileURLToPath 转为文件路径。
    const templateDir = path.resolve(
      fileURLToPath(import.meta.url),
      "../../../",
      "./src/my-create-vite/",
      `template-${template}`
    );
    const renameFiles: Record<string, any> = {
      _gitignore: ".gitignore",
    };
    // write 方法如果传入了 content，就是用 fs.writeFileSync 写入文件。 // 否则就是复制
    const write = (file: string, content?: string) => {
      const targetPath = path.join(root, renameFiles[file] ?? file);
      if (content) {
        fs.writeFileSync(targetPath, content);
      } else {
        copy(path.join(templateDir, file), targetPath);
      }
    };

    function copyDir(srcDir: string, destDir: string) {
      fs.mkdirSync(destDir, { recursive: true });
      for (const file of fs.readdirSync(srcDir)) {
        const srcFile = path.resolve(srcDir, file);
        const destFile = path.resolve(destDir, file);
        copy(srcFile, destFile);
      }
    }

    // fs 模版有 copyFileSync 的方法，但没有 copyDir 的方法，所以我们要自己实现目录的复制
    //  用 fs.stateSync 拿到这个路径是文件还是目录，如果是目录，就先递归创建这个目标目录，然后依次复制源目录下的文件。
    function copy(src: string, dest: string) {
      const stat = fs.statSync(src);
      if (stat.isDirectory()) {
        copyDir(src, dest);
      } else {
        fs.copyFileSync(src, dest);
      }
    }

    // 如果目标目录不存在，就用 mkdirSync 创建。
    if (!fs.existsSync(root)) {
      fs.mkdirSync(root, { recursive: true });
    }
    // 用 readdirSync 读取模版目录下的文件，依次写入目标目录。
    const files = fs.readdirSync(templateDir);
    for (const file of files) {
      write(file);
    }
    const cdProjectName = path.relative(process.cwd(), root);
    console.log(`\nDone. Now run:\n`);
    if (root !== process.cwd()) {
      console.log(
        `  cd ${
          cdProjectName.includes(" ") ? `"${cdProjectName}"` : cdProjectName
        }`
      );
    }
    console.log(`  npm install`);
    console.log(`  npm run dev`);
    console.log();
  } catch (cancelled: any) {
    console.log(cancelled.message);
    return;
  }
}

init().catch((e) => {
  console.error(e);
});
